import AppKit
import Commander
import CoreGraphics
import Foundation
import PeekabooCore
import PeekabooFoundation

/// Moves the mouse cursor to specific coordinates or UI elements.
@available(macOS 14.0, *)
@MainActor
struct MoveCommand: ErrorHandlingCommand, OutputFormattable {
    @Argument(help: "Coordinates as x,y (e.g., 100,200)")
    var coordinates: String?

    @Option(help: "Move to element by text/label")
    var to: String?

    @Option(help: "Move to element by ID (e.g., B1, T2)")
    var id: String?

    @Flag(help: "Move to screen center")
    var center = false

    @Flag(help: "Use smooth movement animation")
    var smooth = false

    @Option(help: "Movement duration in milliseconds (default: 500 for smooth, 0 for instant)")
    var duration: Int?

    @Option(help: "Number of steps for smooth movement (default: 20)")
    var steps: Int = 20

    @Option(help: "Movement profile: linear (default) or human.")
    var profile: String?

    @Option(help: "Session ID for element resolution")
    var session: String?
    @RuntimeStorage private var runtime: CommandRuntime?

    private var resolvedRuntime: CommandRuntime {
        guard let runtime else {
            preconditionFailure("CommandRuntime must be configured before accessing runtime resources")
        }
        return runtime
    }

    private var services: any PeekabooServiceProviding { self.resolvedRuntime.services }
    private var logger: Logger { self.resolvedRuntime.logger }
    var outputLogger: Logger { self.logger }
    var jsonOutput: Bool { self.resolvedRuntime.configuration.jsonOutput }

    mutating func validate() throws {
        // Ensure at least one target is specified
        guard self.center || self.coordinates != nil || self.to != nil || self.id != nil else {
            throw ValidationError("Specify coordinates, --to, --id, or --center")
        }

        // Validate coordinates format if provided
        if let coordString = coordinates {
            let parts = coordString.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
            guard parts.count == 2,
                  Double(parts[0]) != nil,
                  Double(parts[1]) != nil else {
                throw ValidationError("Invalid coordinates format. Use: x,y")
            }
        }

        if let profileName = self.profile?.lowercased(),
           MovementProfileSelection(rawValue: profileName) == nil {
            throw ValidationError("Invalid profile '\(profileName)'. Use 'linear' or 'human'.")
        }
    }

    @MainActor
    mutating func run(using runtime: CommandRuntime) async throws {
        self.runtime = runtime
        let startTime = Date()
        self.logger.setJsonOutputMode(self.jsonOutput)

        do {
            // Determine target location
            let targetLocation: CGPoint
            let targetDescription: String

            if self.center {
                // Move to screen center
                guard let mainScreen = NSScreen.main else {
                    throw ValidationError("No main screen found")
                }
                let screenFrame = mainScreen.frame
                targetLocation = CGPoint(x: screenFrame.midX, y: screenFrame.midY)
                targetDescription = "Screen center"

            } else if let coordString = coordinates {
                // Parse coordinates
                let parts = coordString.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) }
                let x = Double(parts[0])!
                let y = Double(parts[1])!
                targetLocation = CGPoint(x: x, y: y)
                targetDescription = "Coordinates (\(Int(x)), \(Int(y)))"

            } else if let elementId = id {
                // Move to element by ID
                let sessionId: String? = if let providedSession = session {
                    providedSession
                } else {
                    await self.services.sessions.getMostRecentSession()
                }
                guard let activeSessionId = sessionId else {
                    throw PeekabooError.sessionNotFound("No session found")
                }

                guard let detectionResult = try? await self.services.sessions
                    .getDetectionResult(sessionId: activeSessionId),
                    let element = detectionResult.elements.findById(elementId)
                else {
                    throw PeekabooError.elementNotFound("Element with ID '\(elementId)' not found")
                }

                targetLocation = CGPoint(x: element.bounds.midX, y: element.bounds.midY)
                targetDescription = self.formatElementInfo(element)

            } else if let query = to {
                // Find element by text/query
                let sessionId: String? = if let providedSession = session {
                    providedSession
                } else {
                    await self.services.sessions.getMostRecentSession()
                }
                guard let activeSessionId = sessionId else {
                    throw PeekabooError.sessionNotFound("No session found")
                }

                // Wait for element to be available
                let waitResult = try await AutomationServiceBridge.waitForElement(
                    automation: self.services.automation,
                    target: .query(query),
                    timeout: 5.0,
                    sessionId: activeSessionId
                )

                guard waitResult.found, let element = waitResult.element else {
                    throw PeekabooError.elementNotFound(
                        "No element found matching '\(query)'"
                    )
                }

                targetLocation = CGPoint(x: element.bounds.midX, y: element.bounds.midY)
                targetDescription = self.formatElementInfo(element)

            } else {
                throw ValidationError("Specify coordinates, --to, --id, or --center")
            }

            // Get current mouse location for distance calculation
            let currentLocation = CGEvent(source: nil)?.location ?? CGPoint.zero
            let distance = hypot(
                targetLocation.x - currentLocation.x,
                targetLocation.y - currentLocation.y
            )

            let movement = self.resolveMovementParameters(
                profileSelection: self.selectedProfile,
                distance: distance
            )

            // Perform the movement
            try await AutomationServiceBridge.moveMouse(
                automation: self.services.automation,
                to: targetLocation,
                duration: movement.duration,
                steps: movement.steps,
                profile: movement.profile
            )
            AutomationEventLogger.log(
                .cursor,
                "move target=\(targetDescription) duration=\(movement.duration)ms steps=\(movement.steps) "
                    + "profile=\(movement.profileName)"
            )

            // Output results
            let result = MoveResult(
                success: true,
                targetLocation: targetLocation,
                targetDescription: targetDescription,
                fromLocation: currentLocation,
                distance: distance,
                duration: movement.duration,
                smooth: movement.smooth,
                profile: movement.profileName,
                executionTime: Date().timeIntervalSince(startTime)
            )
            output(result) {
                print("✅ Mouse moved successfully")
                print("🎯 Target: \(targetDescription)")
                print("📍 Location: (\(Int(targetLocation.x)), \(Int(targetLocation.y)))")
                print("📏 Distance: \(Int(distance)) pixels")
                print("🧭 Profile: \(movement.profileName.capitalized)")
                if movement.smooth {
                    print("🎬 Animation: \(movement.duration)ms with \(movement.steps) steps")
                }
                print("⏱️  Completed in \(String(format: "%.2f", Date().timeIntervalSince(startTime)))s")
            }

        } catch {
            self.handleError(error)
            throw ExitCode.failure
        }
    }

    private func formatElementInfo(_ element: DetectedElement) -> String {
        let roleDescription = element.type.rawValue.replacingOccurrences(of: "_", with: " ").capitalized
        let label = element.label ?? element.value ?? element.id
        return "\(roleDescription): \(label)"
    }

    private var selectedProfile: MovementProfileSelection {
        guard let profileName = self.profile?.lowercased(),
              let selection = MovementProfileSelection(rawValue: profileName) else {
            return .linear
        }
        return selection
    }

    private func resolveMovementParameters(
        profileSelection: MovementProfileSelection,
        distance: CGFloat
    ) -> MovementParameters {
        switch profileSelection {
        case .linear:
            let resolvedDuration: Int = if let customDuration = self.duration {
                customDuration
            } else {
                self.smooth ? 500 : 0
            }
            let resolvedSteps = self.smooth ? max(self.steps, 1) : 1
            return MovementParameters(
                profile: .linear,
                duration: resolvedDuration,
                steps: resolvedSteps,
                smooth: self.smooth,
                profileName: profileSelection.rawValue
            )
        case .human:
            let resolvedDuration = self.duration ?? self.defaultHumanDuration(for: distance)
            let resolvedSteps = max(self.steps, self.defaultHumanSteps(for: distance))
            return MovementParameters(
                profile: .human(),
                duration: resolvedDuration,
                steps: resolvedSteps,
                smooth: true,
                profileName: profileSelection.rawValue
            )
        }
    }

    private func defaultHumanDuration(for distance: CGFloat) -> Int {
        let distanceFactor = log2(Double(distance) + 1) * 90
        let perPixel = Double(distance) * 0.45
        let estimate = 240 + distanceFactor + perPixel
        return min(max(Int(estimate), 280), 1700)
    }

    private func defaultHumanSteps(for distance: CGFloat) -> Int {
        let scaled = Int(distance * 0.35)
        return min(max(scaled, 30), 120)
    }
}

// MARK: - JSON Output Structure

struct MoveResult: Codable {
    let success: Bool
    let targetLocation: [String: Double]
    let targetDescription: String
    let fromLocation: [String: Double]
    let distance: Double
    let duration: Int
    let smooth: Bool
    let profile: String
    let executionTime: TimeInterval

    init(
        success: Bool,
        targetLocation: CGPoint,
        targetDescription: String,
        fromLocation: CGPoint,
        distance: Double,
        duration: Int,
        smooth: Bool,
        profile: String,
        executionTime: TimeInterval
    ) {
        self.success = success
        self.targetLocation = ["x": targetLocation.x, "y": targetLocation.y]
        self.targetDescription = targetDescription
        self.fromLocation = ["x": fromLocation.x, "y": fromLocation.y]
        self.distance = distance
        self.duration = duration
        self.smooth = smooth
        self.profile = profile
        self.executionTime = executionTime
    }
}

// MARK: - Conformances

@MainActor
extension MoveCommand: ParsableCommand {
    nonisolated(unsafe) static var commandDescription: CommandDescription {
        MainActorCommandDescription.describe {
            CommandDescription(
                commandName: "move",
                abstract: "Move the mouse cursor to coordinates or UI elements",
                discussion: """
                    The 'move' command positions the mouse cursor at specific locations or
                    on UI elements detected by 'see'. Supports instant and smooth movement.

                    EXAMPLES:
                      peekaboo move 100,200                 # Move to coordinates
                      peekaboo move --to "Submit Button"    # Move to element by text
                      peekaboo move --id B3                 # Move to element by ID
                      peekaboo move 500,300 --smooth        # Smooth movement
                      peekaboo move --center                # Move to screen center

                    MOVEMENT MODES:
                      - Instant (default): Immediate cursor positioning
                      - Smooth: Animated movement with configurable duration
                      - Human: Natural arcs with eased velocity, enable via '--profile human'

                    ELEMENT TARGETING:
                      When targeting elements, the cursor moves to the element's center.
                      Use element IDs from 'see' output for precise targeting.
                """,

                showHelpOnEmptyInvocation: true
            )
        }
    }
}

extension MoveCommand: AsyncRuntimeCommand {}

@MainActor
extension MoveCommand: CommanderBindableCommand {
    mutating func applyCommanderValues(_ values: CommanderBindableValues) throws {
        self.coordinates = try values.decodeOptionalPositional(0, label: "coordinates")
        self.to = values.singleOption("to")
        self.id = values.singleOption("id")
        self.center = values.flag("center")
        self.smooth = values.flag("smooth")
        if let duration: Int = try values.decodeOption("duration", as: Int.self) {
            self.duration = duration
        }
        if let steps: Int = try values.decodeOption("steps", as: Int.self) {
            self.steps = steps
        }
        self.session = values.singleOption("session")
        self.profile = values.singleOption("profile")
    }
}

private enum MovementProfileSelection: String {
    case linear
    case human
}

private struct MovementParameters {
    let profile: MouseMovementProfile
    let duration: Int
    let steps: Int
    let smooth: Bool
    let profileName: String
}
